package com.googlecode.rjm.model;

import java.util.ArrayList;
import java.util.Date;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;

import com.googlecode.rjm.model.journal.IssueJournal;
import com.googlecode.rjm.model.journal.JournalDetail;
import com.googlecode.rjm.model.journal.Journalized;
import com.googlecode.rjm.model.journal.StatusJournalDetail;
import com.googlecode.rjm.util.DateUtil;

@Entity
@Table(name = "issues")
public class Issue extends Persistent implements Comparable<Issue>, Journalized<IssueJournal> {

	private static final long serialVersionUID = 7249808458233501053L;

	class Transition {
		Date date;
		Status from;
		Status to;
	}

	@Id
	private Integer id;

	@ManyToOne
	@JoinColumn(name = "project_id")
	private Project project;

	@ManyToOne
	@JoinColumn(name = "tracker_id")
	private Tracker tracker;

	@ManyToOne
	@JoinColumn(name = "status_id")
	private Status status;

	@ManyToOne
	@JoinColumn(name = "assigned_to_id")
	private User assignedTo;

	@ManyToOne
	@JoinColumn(name = "author_id")
	private User author;

	@ManyToOne
	@JoinColumn(name = "parent_id")
	private Issue parent;

	@ManyToOne
	@JoinColumn(name = "root_id")
	private Issue root;

	private String subject;

	private String description;

	@Column(name = "created_on")
	private Date createdOn;

	@Column(name = "start_date")
	private Date startDate;

	@Column(name = "due_date")
	private Date dueDate;

	@Column(name = "done_ratio")
	private Integer doneRatio;

	@Column(name = "estimated_hours")
	private Double estimatedHours;

	@OneToMany(mappedBy = "issue")
	@OrderBy(value = "creationDate")
	private List<IssueJournal> journals;

	@ManyToOne
	@JoinColumn(name = "fixed_version_id")
	private Version version;

	@OneToMany(mappedBy = "issue")
	@OrderBy(value = "date")
	private List<SpentTime> spentTime;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public Project getProject() {
		return project;
	}

	public void setProject(Project project) {
		this.project = project;
	}

	public Tracker getTracker() {
		return tracker;
	}

	public void setTracker(Tracker tracker) {
		this.tracker = tracker;
	}

	public Status getStatus() {
		return status;
	}

	public void setStatus(Status status) {
		this.status = status;
	}

	public User getAssignedTo() {
		return this.assignedTo;
	}

	public void setAssignedTo(User assignedTo) {
		this.assignedTo = assignedTo;
	}

	public User getAuthor() {
		return author;
	}

	public void setAuthor(User author) {
		this.author = author;
	}

	public Issue getParent() {
		return parent;
	}

	public void setParent(Issue parent) {
		this.parent = parent;
	}

	public Issue getRoot() {
		return this.root;
	}

	public void setRoot(Issue root) {
		this.root = root;
	}

	public String getSubject() {
		return subject;
	}

	public void setSubject(String subject) {
		this.subject = subject;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public Date getCreatedOn() {
		return this.createdOn;
	}

	public void setCreatedOn(Date createdOn) {
		this.createdOn = createdOn;
	}

	public Date getStartDate() {
		return startDate;
	}

	public void setStartDate(Date startDate) {
		this.startDate = startDate;
	}

	public Date getDueDate() {
		return dueDate;
	}

	public void setDueDate(Date dueDate) {
		this.dueDate = dueDate;
	}

	public Integer getDoneRatio() {
		return doneRatio;
	}

	public void setDoneRatio(Integer doneRatio) {
		this.doneRatio = doneRatio;
	}

	public Double getEstimatedHours() {
		return estimatedHours;
	}

	public void setEstimatedHours(Double estimatedHours) {
		this.estimatedHours = estimatedHours;
	}

	public List<IssueJournal> getJournals() {
		return this.journals;
	}

	public void setJournals(List<IssueJournal> journals) {
		this.journals = journals;
	}

	public void setVersion(Version version) {
		this.version = version;
	}

	public Version getVersion() {
		return this.version;
	}

	public List<SpentTime> getSpentTime() {
		return this.spentTime;
	}

	public void setSpentTime(List<SpentTime> spentTime) {
		this.spentTime = spentTime;
	}

	/**
	 * Look up in the issue's journals the date of the last transition to a given status.
	 * 
	 * @param s
	 *            The status to be looked.
	 * @return The last transition date to this status.
	 */
	public Date getLastTransitionForStatus(Status s) {
		List<Status> toList = new ArrayList<Status>();
		toList.add(s);
		return getTransitionFromStatusToStatus(null, toList, false, true, false);
	}

	/**
	 * Look up in the issue's journals the date of the first transition to a given status.
	 * 
	 * @param s
	 *            The status to be looked.
	 * @return The first transition date to this status.
	 */
	public Date getFirstTransitionForStatus(Status s) {
		List<Status> toList = new ArrayList<Status>();
		toList.add(s);
		return getTransitionFromStatusToStatus(null, toList, true, true, false);
	}

	public Date getTransitionFromStatusToStatus(List<Status> fromList, List<Status> toList, boolean first, boolean acceptNullableFrom, boolean acceptNullableTo) {
		Date ret = null;

		Deque<Transition> transitionList = getTransitions();

		for (Iterator<Transition> iterator = first ? transitionList.iterator() : transitionList.descendingIterator(); iterator.hasNext();) {
			Transition transition = iterator.next();
			boolean transitionFrom = fromList == null || fromList.contains(transition.from) || (acceptNullableFrom && transition.from == null);
			boolean transitionTo = toList == null || toList.contains(transition.to) || (acceptNullableTo && transition.to == null);
			
			if (transitionFrom && transitionTo) {
				ret = transition.date;
				break;
			}
		}

		return ret;
	}

	/**
	 * Get all transitions considering the initial status.
	 * 
	 * @return The transitions list.
	 */
	public Deque<Transition> getTransitions() {
		Deque<Transition> transitionList = new LinkedList<Transition>();
		Status fisrtOldValue = null;

		for (IssueJournal issueJournal : this.getJournals()) {
			for (JournalDetail<?> journalDetail : issueJournal.getDetails()) {
				if (journalDetail instanceof StatusJournalDetail) {
					StatusJournalDetail statusJournalDetail = (StatusJournalDetail) journalDetail;
					Transition newTransition = new Transition();
					newTransition.date = issueJournal.getCreationDate();
					newTransition.from = statusJournalDetail.getOldTypedValue();
					newTransition.to = statusJournalDetail.getTypedValue();
					transitionList.add(newTransition);
					if (fisrtOldValue == null) {
						fisrtOldValue = statusJournalDetail.getOldTypedValue();
					}
				}
			}
		}

		Transition firstTransition = new Transition();
		firstTransition.date = this.createdOn;
		firstTransition.to = transitionList.size() == 0 ? this.status : fisrtOldValue;

		transitionList.addFirst(firstTransition);
		return transitionList;
	}

	public Status getStatusOnDate(Date date) {
		if (DateUtil.greater(this.createdOn, date)) {
			return null;
		}

		Status status = this.status;
		
		outter: for (IssueJournal issueJournal : this.getJournals()) {
			for (JournalDetail<?> journalDetail : issueJournal.getDetails()) {
				if (journalDetail instanceof StatusJournalDetail) {
					StatusJournalDetail statusJournalDetais = (StatusJournalDetail) journalDetail;
					if (DateUtil.lessOrEqual(issueJournal.getCreationDate(), date)) {
						status = statusJournalDetais.getTypedValue();
					} else {
						status = statusJournalDetais.getOldTypedValue();
						break outter;
					}
				}
			}
		}
		
		return status;
	}

	/**
	 * Test if the <code>this</code> issue is under other in the tree.
	 * 
	 * @param possibleParent
	 *            The issue being tested as parent.
	 * @return <code>true</code> if this issue is directly or indirectly child of other, <code>false</code> otherwise.
	 */
	public boolean isUnder(Issue possibleParent) {
		if (!this.getRoot().equals(possibleParent.getRoot()))
			return false;

		Issue current = this;
		while (!current.equals(current.getRoot())) {
			if (current.getParent().equals(possibleParent)) {
				return true;
			}
			current = current.getParent();
		}
		return false;
	}

	public boolean isUnder(int possibleParentId) {
		Issue current = this;
		while (!current.equals(current.getRoot())) {
			if (current.getParent().getId().equals(possibleParentId)) {
				return true;
			}
			current = current.getParent();
		}
		return false;
	}

	public int distanceToRoot() {
		int dist = 0;
		Issue current = this;
		while (!current.equals(current.getRoot())) {
			current = current.getParent();
			dist++;
		}
		return dist;
	}

	public String toString() {
		if (getProject() != null && getVersion() != null) {
			return getProject().getName() + " - " + getVersion().getName() + ": " + getSubject();
		} else {
			return getSubject();
		}
	}

	public int compareTo(Issue o) {
		return this.toString().compareTo(o.toString());
	}
}
